<?php
/**
 * Eka Framework 0.3.0
 *
 * @copyright Copyright (c) 2008 Alexey Yu. Chuprina <656b61@gmail.com> (http://php-eka.googlecode.com)
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 *
 * @version $Id: Abstract.php 28 2009-01-21 16:13:16Z 656b61 $
 */

require_once 'Eka/Autoloader.php';

/**
 * Eka_Application
 *
 * @package Application
 */
class Eka_Application
{
	static protected $class = 'Eka_Application';

	/**
	 * Singleton instance
	 *
	 * @var Eka_Application
	 */
	static protected $instance;

	/**
	 * Singleton instance
	 *
	 * @return Eka_Application
	 */
	static public function getInstance() {
		return self::$instance;
	}

	/**
	 * @param string $appDir Applicatio directory
	 * @return Eka_Application
	 */
	static public function init($appDir) {
		if (null !== self::$instance) {
			include 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception('Application is inited');
		}
		self::$instance = new self::$class($appDir);
		return self::$instance;
	}

	/**
	 * Set application class name
	 *
	 * @param string $className
	 */
	static public function setClassName($className)
	{
		self::$class = $className;
	}

################################################################################
	/**
	 * @var boolean
	 */
	protected $dispatched = false;

	/**
	 * @var Eka_Application_Request
	 */
	protected $request;

	/**
	 * @var Eka_Application_Response
	 */
	protected $response;

	/**
	 * @var Eka_Session_Abstract
	 */
	protected $session;

	/**
	 * @var string
	 */
	protected $controllerPath;

	/**
	 * @var string
	 */
	protected $modelPath;

	/**
	 * @var string
	 */
	protected $viewPath;

	/**
	 * @var string
	 */
	protected $viewClassPath;

	/**
	 * @var string
	 */
	protected $cachePath;

	/**
	 * @var string
	 */
	protected $viewExtension = 'phtml';

	/**
	 * Default View class name
	 *
	 * @var strign
	 */
	protected $viewDefaultClass = 'Eka_Application_View';

	/**
	 * @var Eka_Application_Controller_Abstract
	 */
	protected $activeController;

	/**
	 * @var string
	 */
	protected $requestClass = 'Eka_Application_Request';

	/**
	 * @var string
	 */
	protected $responseClass = 'Eka_Application_Response';

	/**
	 * @var string
	 */
	protected $sessionClass = 'Eka_Session';

	/**
	 * @var array
	 */
	protected $sessionConfig = array(
		'name' => '_sid_'
		);

	/**
	 * @var array
	 */
	protected $router;

	/**
	 * Default Content-Type
	 *
	 * If Content-Type header not set id Response object, send this value
	 *
	 * @var strign
	 */
	protected $defaultContenType = 'text/html; charset=utf-8';

	/**
	 * Paths to error pages (404, 500)
	 *
	 * @var array
	 */
	protected $errorPaths = array();

	/**
	 * Application position in request path
	 *
	 * @var int
	 */
	private $deep = 0;

	protected $dbAdapters = array();

	protected function __construct($appDir, $config = null)
	{
		mb_internal_encoding('utf-8');
		mb_language('uni');

		$appDir = realpath($appDir);
		if (!$appDir) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception("Bad application path '$appDir'");
		}
		$appDir .= DIRECTORY_SEPARATOR;
		$this->controllerPath = $appDir . 'controllers';
		$this->modelPath = $appDir . 'lib'. DIRECTORY_SEPARATOR . 'models';
		$this->viewPath = $appDir . 'views';
		$this->viewClassPath = $appDir . 'lib' . DIRECTORY_SEPARATOR . 'views';
		$this->cachePath = $appDir . 'cache';

		set_include_path(
			$appDir . 'lib/' . PATH_SEPARATOR .
			$this->modelPath . PATH_SEPARATOR .
			get_include_path());

		Eka_Autoloader::register(array($this, 'mvcAutoloadHandler'));

		if (null !== $config) {
			$this->configure($config);
		}

		$configureFile = $this->cachePath . DIRECTORY_SEPARATOR . 'application.php';
		if (file_exists($configureFile)) {
			include_once $configureFile;
		}
	}

	/**
	 * Enforce singleton, disallow cloning
	 */
	private final function __clone()
	{}

	public function configure($config)
	{
		if ($this->dispatched) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception('Can not configure, because application running');
		}
		foreach ($config as $key => $value) {
			switch ($key) {
				case 'error_path_404':
					$this->errorPaths[404] = $value;
					break;
				case 'error_path_403':
					$this->errorPaths[403] = $value;
					break;
				case 'error_path_500':
					$this->errorPaths[500] = $value;
					break;
				case 'Temply':
					if ($value) {
						if (isset($config['view_class'])) {
							$this->viewDefaultClass = $config['view_class'];
						}
						if (isset($config['view_extension'])) {
							$this->viewExtension = $config['view_extension'];
						}
						$this->useTemply();
					}
					break;
				case 'view_extension':
					$this->viewExtension = $value;
					break;
				case 'view_class':
					$this->viewDefaultClass = $value;
					break;
				case 'session_config':
					$this->sessionConfig = array_merge($this->sessionConfig, $value);
					break;
				case 'router':
					$this->router = $value;
					break;
			}
		}
	}

	/**
	 * Register db adapter
	 *
	 * @param Eka_Db_Adapter_Abstract $adapter
	 * @param string $name
	 */
	public function registerDbAdapter(Eka_Db_Adapter_Abstract $adapter, $name = 'default')
	{
		if (isset($this->dbAdapters[$name])) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception("DB adapter name '$name' is already registered");
		}
		$this->dbAdapters[$name] = $adapter;
	}

	/**
	 * Get registred db adapter
	 *
	 * @param string $name
	 * @return Eka_Db_Adapter_Abstract
	 */
	public function getDbAdapter($name = 'default')
	{
		return isset($this->dbAdapters[$name]) ? $this->dbAdapters[$name] : null;
	}

	/**
	 * Get default view class name
	 *
	 * @return string
	 */
	public function getDefaultViewClass()
	{
		return $this->viewDefaultClass;
	}

	/**
	 * Get default Content-Type
	 *
	 * @return string
	 */
	public function getDefaultContenType()
	{
		return $this->defaultContenType;
	}

	/**
	 * @return string
	 */
	public function getViewPath()
	{
		return $this->viewPath;
	}

	public function getViewExtension()
	{
		return $this->viewExtension;
	}

	public function getModelPath()
	{
		return $this->modelPath;
	}

	/**
	 * Send 403 error
	 */
	public function error403()
	{
		$this->error(403);
	}

	/**
	 * Send 404 error
	 */
	public function error404()
	{
		$this->error(404);
	}

	/**
	 * Send 500 error
	 */
	public function error500()
	{
		$this->error(500);
	}

	/**
	 * Helper for error404() and error500()
	 *
	 * @param int $code
	 * @throws Eka_Application_Error
	 */
	protected function error($code)
	{
		$this->response->setCode($code);
		if (isset($this->errorPaths[$code])) {
			$this->route($this->errorPaths[$code]);
		}
		$this->stop();
	}

	public function stop($text = null)
	{
		if (null !== $text) {
			$this->response->setBody($text);
		}
		$this->postDispatch();
		exit;
	}

	/**
	 * MVC class loader
	 *
	 * @param string $class
	 */
	public function mvcAutoloadHandler($class)
	{
		switch (true) {
			case 0 === strpos($class, 'Controller_'):
				$this->loadController($class);
				break;
			/*case 0 === strpos($class, 'Model_'):
				$this->loadModel($class);
				break;*/
			case 0 === strpos($class, 'View_'):
				$this->loadView($class);
				break;
			default:
				return;
		}
	}

	/**
	 * Load controller by name or class name
	 *
	 * @param strgin $name
	 * @return strgin
	 *
	 * @throws Eka_Application_Exception
	 */
	public function loadController($name)
	{
		$class = $this->mvcLoader('Controller', $this->controllerPath, $name);
		if (!$class) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception("Controller '$name' not fount");
		}
		return $class;
	}

	/*
	public function loadModel($name)
	{
		$class = $this->mvcLoader('Model', $this->modelPath, $name);
		if (!$class) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception("Model '$name' not fount");
		}
		return $class;
	}
	*/

	/**
	 * Load view by name or class namse
	 *
	 * @param strgin $name
	 * @return strgin
	 *
	 * @throws Eka_Application_Exception
	 */
	public function loadView($name)
	{
		$class = $this->mvcLoader('View', $this->viewClassPath, $name);
		if (!$class) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception("View '$name' not fount");
		}
		return $class;
	}

	/**
	 * Helper for loadController(), loadView() // loadModel()
	 *
	 * @param string $prefix
	 * @param string $path
	 * @param string $name
	 * @return strgin|boolean
	 */
	protected function mvcLoader($prefix, $path, $name)
	{
		$prefix .= '_';
		if (0 === strpos($name, $prefix)) {
			$class = $name;
			$name = substr($name, strlen($prefix));
		} else {
			$class = $prefix . $name;
		}

		if (class_exists($class, false)) {
			return $class;
		}

		$file = $path . DIRECTORY_SEPARATOR . str_replace('_', DIRECTORY_SEPARATOR, $name) . '.php';
		if ($prefix === 'Controller_' && !file_exists($file)) {
			$file = explode('_', $name);
			$last = count($file) - 1;
			$file[$last] = '_' . $file[$last];
			$file = $path . DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $file) . '.php';
		}
		if (file_exists($file)) {
			include_once($file);
			if (class_exists($class, false)) {
				return $class;
			}
		}

		return false;
	}

	public function redirect($url)
	{
		$this->response->redirect($url, true);
	}

	/**
	 * Run Application
	 */
	public function dispatch(Eka_Application_Request $request = null,
		Eka_Application_Response $response = null,
		Eka_Session_Abstract $session = null,
		$deep = null
	) {
		if ($this->dispatched) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception('Application is already running');
		}
		$this->dispatched = true;

		if (null !== $request) {
			$this->request = $request;
		} else if (null === $this->request) {
			$this->request = new $this->requestClass();
		}

		if (null !== $response) {
			$this->response = $response;
		} else if (null === $this->response) {
			$this->response = new $this->responseClass();
		}

		if (null !== $session) {
			$this->session = $session;
		} else if (null === $this->session) {
			$this->session = new $this->sessionClass($this->sessionConfig);
		}

		if (null === $deep) {
			foreach (explode('/', dirname($this->request->getServer('SCRIPT_NAME'))) as $elm) {
				if ($elm !== '' && $elm !== DIRECTORY_SEPARATOR) {
					++$this->deep;
				}
			}
		} else {
			$this->deep = $deep;
		}

		$this->route();

		$this->postDispatch();
	}

	public function postDispatch()
	{
		if ($this->defaultContenType && !$this->response->hasHeader('Content-Type')) {
			$this->response->setContentTypeHeader($this->defaultContenType);
		}

		/*if (!$this->response->hasHeader('Expires')) {
			$this->response->setHeader('Expires', 'Mon, 1 Jan 2000 00:00:00 GMT');
		}*/
		if (!$this->response->hasHeader('Last-Modified')) {
			$this->response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s').' GMT');
		}

		$this->response->send();
	}

	/**
	 * Route request to action
	 *
	 * @param string $path Path to action
	 * @param bool $downToRoot
	 */
	public function route($path = null)
	{
		if (!$this->dispatched) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception("Unable to route because application is not running");
		}

		if ($this->activeController) {
			$this->activeController->stop();
		}

		if ($path !== null) {
			$this->request->setPath($path, $this->deep);
		}

		if ($this->router) {
			$path = $this->request->getPath($this->deep);
			$matches = array();
			$ignore = array();

			reset($this->router);
			while (list($key, $instructions) = each($this->router)) {
				#TODO проверить что быстрее in_array($key, $ignore) vs isset($ignore[$key])
				if (in_array($key, $ignore)) continue;
				$pattern = '`' . $instructions['pattern'] . '`i';
				if (preg_match($pattern, $path, $matches)) {
					if (isset($instructions['replace'])) {
						$path = preg_replace($pattern, $instructions['replace'], $path);
						$this->request->setPath($path, $this->deep);
						$ignore[] = $key;
						reset($this->router);
					}
					if (!empty($instructions['params'])) {
						array_shift($matches);
						$params = array();
						foreach ($instructions['params'] as $k => $v) {
							if (array_key_exists($k, $matches)) {
								$params[$v] = $matches[$k];
							}
						}
						$this->request->setParams($params);
					}
					if (isset($instructions['stop']) && $instructions['stop']) {
						break;
					}
				}
			}
		}

		$pathArray = $this->request->getFormatedPathSegments($this->deep);

		if (count($pathArray) > 0) {

			$segments = array();
			$path = $this->controllerPath;
			foreach ($pathArray as $segment) {
				if (null === $segment) {
					break;
				}
				$segments[] = $segment;
				$path .= DIRECTORY_SEPARATOR . $segment;
				if (is_dir($path)) {
					continue;
				}
				break;
			}
			do {
				$path = $this->controllerPath . DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $segments);
				if (is_file($path . DIRECTORY_SEPARATOR . 'Index.php')) {
					$segments[] = 'Index';
					break;
				}
				if (is_file($path . '.php')) {
					break;
				}
			} while (array_pop($segments));

			$class = join('_', $segments);
			$deep = count($segments) + $this->deep;
			if (end($segments) === 'Index') {
				$deep --;
			}

			unset($path, $segments, $segment);

		} else {
			$class = 'Index';
			$deep = $this->deep;
		}
		$class = $this->loadController($class);
		$this->activeController = new $class($this, $deep);
		$this->activeController->dispatch();
	}

	/**
	 * Set Request object
	 *
	 * @param Eka_Application_Request $request
	 * @throws Eka_Application_Exception
	 */
	public function setRequest(Eka_Application_Request $request)
	{
		if ($this->dispatched) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception('Can not set request, because application running');
		}
		$this->request = $request;
	}

	/**
	 * Return Request object
	 *
	 * @return Eka_Application_Request
	 */
	public function getRequest()
	{
		return $this->request;
	}

	/**
	 * Set Response object
	 *
	 * @param Eka_Application_Response $response
	 * @throws Eka_Application_Exception
	 */
	public function setResponse(Eka_Application_Response $response)
	{
		if ($this->dispatched) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception('Can not set response, because application running');
		}
		$this->response = $response;
	}

	/**
	 * Return Response object
	 *
	 * @return Eka_Application_Response
	 */
	public function getResponse()
	{
		return $this->response;
	}

	/**
	 * Set Session object
	 *
	 * @param Eka_Session_Abstract $session
	 * @throws Eka_Application_Exception
	 */
	public function setSession(Eka_Session_Abstract $session)
	{
		if ($this->dispatched) {
			include_once 'Eka/Application/Exception.php';
			throw new Eka_Application_Exception('Can not set response, because application running');
		}
		$this->session = $session;
	}

	/**
	 * Return Session object
	 *
	 * @return Eka_Session_Abstract
	 */
	public function getSession()
	{
		return $this->session;
	}

	/**
	 * Configure Temply for this Application
	 */
	public function useTemply()
	{
		Temply::setTemplatePath($this->viewPath);
		Temply::setTemplateExtension($this->viewExtension);

		$cachePath = $this->cachePath . DIRECTORY_SEPARATOR . 'Temply';
		if (!is_dir($cachePath)) {
			mkdir($cachePath, 0777);
			chmod($cachePath, 0777);
		}
		Temply::setCachePath($cachePath);
		$this->viewDefaultClass = 'Eka_Application_View_Temply';
	}
}